package com.mazaiting.minio.service.impl;

import cn.hutool.core.convert.Convert;
import com.mazaiting.common.core.domain.result.ResultCode;
import com.mazaiting.common.core.utils.DateTimeUtil;
import com.mazaiting.common.core.utils.StringUtil;
import com.mazaiting.minio.config.MinioProperties;
import com.mazaiting.minio.constant.MinioConstant;
import com.mazaiting.minio.domain.FileInfo;
import com.mazaiting.minio.domain.PolicyInfo;
import com.mazaiting.minio.ex.MinioException;
import com.mazaiting.minio.service.IMinioService;
import com.mazaiting.minio.utils.ImgUtil;
import io.minio.*;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.URL;
import java.time.ZonedDateTime;
import java.util.*;

/**
 * minio 操作服务实现类
 *
 * @author mazaiting
 * @create_time 2022/10/7 13:31
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MinioServiceImpl implements IMinioService {

    private final MinioClient minioClient;

    private final MinioProperties minioProperties;

    /**
     * 文件格式
     */
    private static final String URL_FORMAT = "%s/%s/%s";

    @Override
    public boolean bucketExists(String bucketName) throws MinioException {
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            log.error("检查桶是否存在异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_CHECK_BUCKET_ERROR);
        }
    }

    @Override
    public void createBucket(String bucketName) throws MinioException {
        if (!bucketExists(bucketName)) {
            try {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                // 设置存储桶访问权限为PUBLIC， 如果不配置，则新建的存储桶默认是PRIVATE，则存储桶文件会拒绝访问 Access Denied
                SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs
                        .builder().bucket(bucketName)
                        .config(publicBucketPolicy(bucketName))
                        .build();
                minioClient.setBucketPolicy(setBucketPolicyArgs);
            } catch (Exception e) {
                log.error("创建存储桶失败: {}.", e.getMessage());
                throw new MinioException(ResultCode.MINIO_CREATE_BUCKET_FAILED);
            }
        } else {
            log.warn("存储桶已存在: {}.", bucketName);
        }
    }

    @Override
    public String putObject(MultipartFile file) throws MinioException {
        // 判断文件或文件名是否存在
        if (Objects.isNull(file) || !StringUtils.hasLength(file.getOriginalFilename())) {
            throw new MinioException(ResultCode.MINIO_PUT_OBJECT_EMPTY);
        }
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(file.getOriginalFilename());
        // 开始上传
        putMultipartFile(minioProperties.getBucketName(), fileName, file);
        return String.format(URL_FORMAT, minioProperties.getUrl(), minioProperties.getBucketName(), fileName);
    }

    @Override
    public String putObject(String objectName, InputStream inputStream, String contentType) throws MinioException {
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(objectName);
        // 开始上传
        putInputStream(minioProperties.getBucketName(), fileName, inputStream, contentType);
        return String.format(URL_FORMAT, minioProperties.getUrl(), minioProperties.getBucketName(), fileName);
    }

    @Override
    public String putObject(String objectName, byte[] bytes, String contentType) throws MinioException {
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(objectName);
        // 开始上传
        putBytes(minioProperties.getBucketName(), fileName, bytes, contentType);
        return String.format(URL_FORMAT, minioProperties.getUrl(), minioProperties.getBucketName(), fileName);
    }

    @Override
    public String putObject(String objectName, MultipartFile file) throws MinioException {
        // 给文件名添加时间戳防止重复
        objectName = getFileName(objectName);
        // 开始上传
        putMultipartFile(minioProperties.getBucketName(), objectName, file);
        return String.format(URL_FORMAT, minioProperties.getUrl(), minioProperties.getBucketName(), objectName);
    }

    @Override
    public String putObject(String bucketName, String objectName, MultipartFile file) throws MinioException {
        // 先创建桶
        createBucket(bucketName);
        // 给文件名添加时间戳防止重复
        objectName = getFileName(objectName);
        // 开始上传
        putMultipartFile(bucketName, objectName, file);
        return String.format(URL_FORMAT, minioProperties.getUrl(), bucketName, objectName);
    }

    @Override
    public String putObject(String bucketName, String objectName, InputStream inputStream, String contentType) throws MinioException {
        // 先创建桶
        createBucket(bucketName);
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(objectName);
        // 开始上传
        putInputStream(bucketName, fileName, inputStream, contentType);
        return String.format(URL_FORMAT, minioProperties.getUrl(), bucketName, fileName);
    }

    @Override
    public String putObject(String bucketName, String objectName, byte[] bytes, String contentType) throws MinioException {
        // 先创建桶
        createBucket(bucketName);
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(objectName);
        // 开始上传
        putBytes(bucketName, fileName, bytes, contentType);
        return String.format(URL_FORMAT, minioProperties.getUrl(), bucketName, fileName);
    }

    @Override
    public String putObject(String objectName, File file, String contentType) throws MinioException {
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(objectName);
        // 开始上传
        putFile(minioProperties.getBucketName(), fileName, file, contentType);
        return String.format(URL_FORMAT, minioProperties.getUrl(), minioProperties.getBucketName(), fileName);
    }

    @Override
    public String putObject(String bucketName, String objectName, File file, String contentType) throws MinioException {
        // 先创建桶
        createBucket(bucketName);
        // 给文件名添加时间戳防止重复
        String fileName = getFileName(objectName);
        // 开始上传
        putFile(bucketName, fileName, file, contentType);
        return String.format(URL_FORMAT, minioProperties.getUrl(), bucketName, fileName);
    }

    @Override
    public Boolean checkFileExists(String objectName) {
        return checkFileExists(minioProperties.getBucketName(), objectName);
    }

    @Override
    public Boolean checkFolderExists(String folderName) {
        return checkFolderExists(minioProperties.getBucketName(), folderName);
    }

    @Override
    public Boolean checkFileExists(String bucketName, String objectName) {
        boolean exist;
        try {
            StatObjectResponse resp = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
            exist = Objects.nonNull(resp);
        } catch (Exception e) {
            log.error("校验文件[{}]-[{}]是否存在异常: {}.", bucketName, objectName, e.getMessage());
            exist = false;
        }
        return exist;
    }

    @Override
    public Boolean checkFolderExists(String bucketName, String folderName) {
        boolean exist = false;
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName)
                            .prefix(folderName).recursive(false).build());
            // 遍历
            for (Result<Item> result : results) {
                Item item = result.get();
                if (item.isDir() && folderName.equals(item.objectName())) {
                    exist = true;
                }
            }
        } catch (Exception e) {
            log.error("校验文件夹[{}]-[{}]是否异常: {}.", bucketName, folderName, e.getMessage());
            exist = false;
        }
        return exist;
    }

    @Override
    public InputStream getObject(String objectName) throws MinioException {
        return getObject(minioProperties.getBucketName(), objectName);
    }

    @Override
    public InputStream getObject(String bucketName, String objectName) throws MinioException {
        try {
            return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            log.error("获取对象[{}]-[{}]异常: {}.", bucketName, objectName, e.getMessage());
            throw new MinioException(ResultCode.MINIO_GET_OBJECT_ERROR);
        }
    }

    @Override
    public InputStream getObjectByUrl(String url) throws MinioException {
        try {
            return new URL(url).openStream();
        } catch (IOException e) {
            log.error("获取对象URL-[{}]异常: {}.", url, e.getMessage());
            throw new MinioException(ResultCode.MINIO_GET_OBJECT_ERROR);
        }
    }

    @Override
    public List<Bucket> getAllBuckets() throws MinioException {
        try {
            return minioClient.listBuckets();
        } catch (Exception e) {
            log.error("获取全部存储桶异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_LIST_BUCKET_ERROR);
        }
    }

    @Override
    public Optional<Bucket> getBucket(String bucketName) throws MinioException {
        try {
            return minioClient.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
        } catch (Exception e) {
            log.error("根据存储通名称获取信息异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_GET_BUCKET_ERROR);
        }
    }

    @Override
    public void removeBucket(String bucketName) throws MinioException {
        try {
            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        } catch (Exception e) {
            log.error("根据存储桶名称删除桶异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_DELETE_BUCKET_ERROR);
        }
    }

    @Override
    public void removeObject(String objectName) throws MinioException {
        removeObject(minioProperties.getBucketName(), objectName);
    }

    @Override
    public void removeObject(String bucketName, String objectName) throws MinioException {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            log.error("删除对象失败, 存储桶[{}], 对象名: [{}], 异常信息: {}.", bucketName, objectName, e.getMessage());
            throw new MinioException(ResultCode.MINIO_REMOVE_OBJECT_ERROR);
        }
    }

    @Override
    public List<FileInfo> listFiles(String bucketName) {
        try {
            // 遍历
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).recursive(true).build());
            // 文件信息
            List<FileInfo> infos = new ArrayList<>();
            // 遍历
            for (Result<Item> result : results) {
                Item item = result.get();
                infos.add(new FileInfo(item.objectName(), item.isDir()));
            }
            return infos;
        } catch (Exception e) {
            log.error("根据桶名获取文件列表失败, 存储桶名: [{}], 异常信息: {}.", bucketName, e.getMessage());
        }
        return Collections.emptyList();
    }

    @Override
    public PolicyInfo policy(String bucketName, String objectName) throws MinioException {
        PostPolicy postPolicy = new PostPolicy(bucketName, ZonedDateTime.now().plusMinutes(10));
        // 获取文件名
        String fileName = getFileName(objectName);
        postPolicy.addEqualsCondition(MinioConstant.KEY, fileName);
        try {
            Map<String, String> map = minioClient.getPresignedPostFormData(postPolicy);
            PolicyInfo policyInfo = new PolicyInfo();
            policyInfo.setHost(minioProperties.getUrl() + "/" + bucketName);
            policyInfo.setKey(fileName);
            policyInfo.setPolicy(map.getOrDefault(MinioConstant.POLICY, ""));
            policyInfo.setXAmzAlgorithm(map.getOrDefault(MinioConstant.X_AMZ_ALGORITHM, ""));
            policyInfo.setXAmzCredential(map.getOrDefault(MinioConstant.X_AMZ_CREDENTIAL, ""));
            policyInfo.setXAmzSignature(map.getOrDefault(MinioConstant.X_AMZ_SIGNATURE, ""));
            policyInfo.setXAmzDate(map.getOrDefault(MinioConstant.X_AMZ_DATE, ""));
            return policyInfo;
        } catch (Exception e) {
            log.error("获取授权异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_POLICY_ERROR);
        }
    }

    /**
     * 上传MultipartFile通用方法
     *
     * @param bucketName 桶名称
     * @param objectName 文件名
     * @param file       文件
     * @return true: 上传成功; 上传失败
     */
    private boolean putMultipartFile(String bucketName, String objectName, MultipartFile file) throws
            MinioException {
        try {
            InputStream inputStream;
            // 是否开启压缩
            if (ImgUtil.isImg(objectName) && minioProperties.isImgCompressionEnabled()) {
                long fileSize = file.getSize();
                log.info("图片({})压缩前大小：{}KB", objectName, fileSize / 1024);
                // 压缩
                float compressQuality = ImgUtil.getCompressQuality(fileSize);
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream(Convert.toInt(fileSize));
                Thumbnails.of(file.getInputStream())
                        .scale(1f) // 图片大小比例
                        .outputQuality(compressQuality) // 图片质量压缩比
                        .toOutputStream(outputStream);
                inputStream = new ByteArrayInputStream(outputStream.toByteArray());
                log.info("图片({})压缩后大小：{}KB", objectName, inputStream.available() / 1024);
            } else {
                inputStream = file.getInputStream();
            }
            return putInputStream(bucketName, objectName, inputStream, file.getContentType());
        } catch (Exception e) {
            log.error("上传文件异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_PUT_OBJECT_ERROR);
        }
    }

    /**
     * 上传InputStream通用方法
     *
     * @param bucketName 桶名称
     * @param objectName 文件名
     * @param is         文件流
     */
    private boolean putInputStream(String bucketName, String objectName, InputStream is, String contentType) throws
            MinioException {
        try {
            ObjectWriteResponse resp = minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .contentType(contentType)
                            .stream(is, is.available(), -1)
                            .build()
            );
            return Objects.nonNull(resp);
        } catch (Exception e) {
            log.error("上传文件异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_PUT_OBJECT_ERROR);
        }
    }

    /**
     * 上传 bytes 通用方法
     *
     * @param bucketName 桶名称
     * @param objectName 文件名
     * @param bytes      字节编码
     */
    private boolean putBytes(String bucketName, String objectName, byte[] bytes, String contentType) throws
            MinioException {
        // 字节转文件流
        try (InputStream is = new ByteArrayInputStream(bytes)) {
            return putInputStream(bucketName, objectName, is, contentType);
        } catch (Exception e) {
            log.error("上传文件异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_PUT_OBJECT_ERROR);
        }
    }

    /**
     * 上传 file 通用方法
     *
     * @param bucketName  存储桶
     * @param objectName  对象名
     * @param file        文件
     * @param contentType 文件类型
     */
    private void putFile(String bucketName, String objectName, File file, String contentType) throws MinioException {
        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .contentType(contentType)
                            .stream(fileInputStream, fileInputStream.available(), -1)
                            .build()
            );
        } catch (Exception e) {
            log.error("上传文件异常: {}.", e.getMessage());
            throw new MinioException(ResultCode.MINIO_PUT_OBJECT_ERROR);
        }
    }

    /**
     * 生成唯一ID
     *
     * @param objectName 对象名
     * @return 文件名
     */
    private static String getFileName(String objectName) {
        //判断文件最后一个点所在的位置
        int lastIndexOf = objectName.lastIndexOf(".");
        // 文件路径
        String filePath = DateTimeUtil.time2String(System.currentTimeMillis(), DateTimeUtil.YYYY_MM_DD_SLASH);
        // uuid
        String uuid = StringUtil.getUUID();
        // 格式化
        if (lastIndexOf == -1) {
            // 2022/10/07/{objectName}_{uuid}
            return String.format("%s/%s_%s", filePath, objectName, uuid);
        } else {
            // 获取文件前缀,已最后一个点进行分割
            String filePrefix = objectName.substring(0, objectName.lastIndexOf("."));
            // 获取文件后缀,已最后一个点进行分割
            String fileSuffix = objectName.substring(objectName.lastIndexOf(".") + 1);
            // 组成唯一文件名: // 2022/10/07/{objectName_prefix}_{uuid}.{objectName_suffix}
            return String.format("%s/%s_%s.%s", filePath, filePrefix, uuid, fileSuffix);
        }
    }


    /**
     * PUBLIC桶策略
     * 如果不配置，则新建的存储桶默认是PRIVATE，则存储桶文件会拒绝访问 Access Denied
     *
     * @param bucketName 存储桶名
     * @return 公共桶策略
     */
    private static String publicBucketPolicy(String bucketName) {
        /*
         * AWS的S3存储桶策略
         * Principal: 生效用户对象
         * Resource:  指定存储桶
         * Action: 操作行为
         */
        return "{\"Version\":\"2012-10-17\","
                + "\"Statement\":[{\"Effect\":\"Allow\","
                + "\"Principal\":{\"AWS\":[\"*\"]},"
                + "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],"
                + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},"
                + "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]},"
                + "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],"
                + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
    }


}
